仓鼠的石子游戏
有$N$个石圈,每个石圈有$A[i]$个石头围成一圈,有两个人轮流对石子进行染色,相邻不能同色,首先不能操作的一方输,求先手必赢还是后手必赢。
这个题叭,说是博弈论,但是比较好推。首先讨论只有一个石圈的情况。那么我们发现对于偶数个的话,我们类比“放盘子问题”,对于先手放的每一个石头,我们都放到过石圈中点的对称位置,那么先手一定先不能走。
再讨论奇数的时候,我们后手每一次都放到先手的相邻位置,那么我们依然必赢。也就是说对于一个石圈,无论是奇数还是偶数(1除外),先手都是必输的。
但是唯一的不同之处就在于1,1的时候先手是必赢的。
那么转而来看多个石圈,你会发现没有任何不同,你可能会认为对于一个圈$A[i]$,先手某一次不放$A[i]$而转战$A[j]$就会改变$A[i]$里面的先后手顺序。但是没有关系,我们后手只需要也转战$A[j]$就可以了。
这个时候我们会发现1仍然是个唯一的特例。所以我们手推几组样例就会发现奇数个1就是先手赢,而偶数个1就是后手赢。
1 |
|
乃爱与城市拥挤程度
给你一棵树,问你每个节点作为根的时候距离小于$K$的节点的个数以及这些节点动态权值的乘积。
这是个换根DP。
首先对于第一问,我们设$Dp[i][j]$表示$i$点在$j$布以内的点数,那么转移的时候就转移到与自己联通的其他节点上,然后我们去除重复计算的j-2步向其他方向扩散的状态,就得到$Dp$方程:
(D表示度。)
对于第二问,我们考虑先随便找到一个根然后进行换根。我们先把随便找到的根进行Dp,然后储存起来。对于每一次换根,假设我们要从$X$转换到$Y$,那么对于$X$我们做一个自下而上的Dp,然后对于$Y$我们再做一个自上而下的Dp,然后合并一下就可以了。
(说白了还是瞎搞)
1 |
|
小w的魔术扑克
$K$具有正反面的卡片,打出时只能选择一面,查询$Q$次,每次问是否能组成$L$到$R$的顺子。
智障死了,这个题。
发现30pts特别好拿,时间不是很充足了就弄了个匈牙利匹配,然后用前缀和特判一下10分的正反相同的情况勉勉强强拿到了40分。考场上只写出来了这些。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
using namespace std ;
typedef long long LL ;
const int MAXN = 100010 ;
const int MAXM = 100010 ;
const int INF = 0x7fffffff ;
int N, K, Q, Girl[MAXN], Use[MAXN], V[MAXN] ;
int Sum[MAXN], F = 1, H[MAXN], Tot ;
struct NODE {
int F, T, Next ;
} E[MAXN << 1 * 10] ;
inline void Add(int F, int T) {
E[++ Tot].F = F ; E[Tot].T = T ;
E[Tot].Next = H[F], H[F] = Tot ;
}
inline int Read() {
int X = 0, F = 1 ; char ch = getchar() ;
while (ch > '9' || ch < '0')
F = (ch == '-' ? - 1 : 1), ch = getchar() ;
while (ch >= '0' && ch <= '9')
X = (X << 1) + (X << 3) + (ch ^ 48), ch = getchar() ;
return X * F ;
}
inline bool Find(int Now) {
for(int i = H[Now] ; i ; i = E[i].Next)
if (! Use[E[i].T]){
Use[E[i].T] = 1 ;
if(! Girl[E[i].T] || Find(Girl[E[i].T])){
Girl[E[i].T] = Now ;
return true ;
}
}
return false;
}
signed main(){
freopen("ex.in", "r", stdin) ;
freopen("ex2.out", "w", stdout) ;
N = Read() ; K = Read() ;
for (int i = 1 ; i <= K ; i ++) {
int A = Read(), B = Read() ;
Add(A, i) ; Add(B, i) ;
if (A != B) F = 0 ;
V[A] = 1 ; V[B] = 1 ;
}
if (F == 1) {
for (int i = 1 ; i <= N ; i ++)
if (V[i]) Sum[i] = Sum[i - 1] + 1 ;
else Sum[i] = Sum[i - 1] ;
Q = Read() ;
for (int i = 1 ; i <= Q ; i ++) {
int L = Read(), R = Read() ;
if (Sum[R] - Sum[L - 1] == R - L + 1)
puts("Yes") ;
else puts("No") ;
}
return 0 ;
}
Q = Read() ;
for (int i = 1 ; i <= Q ; i++) {
memset(Girl, 0, sizeof (Girl)) ;
int L = Read(), R = Read(), Ans = 0 ;
for (int i = L ; i <= R ; i ++) {
memset(Use, 0, sizeof (Use)) ;
Ans += Find(i) ;
}
if (Ans == R - L + 1) printf("Yes\n") ;
else printf("No\n") ;
}
return 0 ;
}
然后我们考虑正解。
如果能想到图论模型,这个题就解了一半了。对于每个面值,由于在一个查询中它最多需要一次。所以我们可以把每个面值都当成是一个节点,对于一张牌,我们都建一条连接它正反两面两个面值的边。建好模型以后,我们不难发现,如果对于一个大小为N的连通块有至少N条边,那么这个连通块一定能满足保证每个面值都能够被提供至少一次。只有一种连通块特殊,也就是当连通块的大小为N,并且它具有N-1条边时,无论怎么调整,都会漏掉一个面值无法打出,大小为N,具有N-1条边的连通块,那也就是树。
所以我们先找树,dfs并查集啥玩意都ok,总之找出所有的树就可以了。
然后我们想这个问题的反面:什么样的顺子是不能满足的,我们发现,如果你的顺子完全包含了一颗树,那这个顺子就由于上述的原因无法构造。问题来了?如何判断一个顺子是否完全包含树?
先求出每一颗树上节点编号的最小值与最大值,构造一个约束线段,约束线段的左端点为树上节点编号的最小值,右端点为树上节点编号的最大值。
接下来问题转化成了线段覆盖问题:
即:给定查询线段l,r,问你查询线段是否至少完整的覆盖了任意一个约束线段。如果存在则输出No,反之输出Yes。
这个就是个经典问题了,按照线段的右端点排序然后离线树状数组。对于每一个约束线段都将它的左端点赋成1,然后查询,查询区间和是否为0即可。
当然你也可以使用桶排序或者链式前向星处理查询,然后离线扫描线for过去维护下界。这样做是近似O(n)复杂度的算法,因为第一步使用了并查集,所以全局复杂度不是O(n),而是O(nlogn),只不过并查集常数太小了所以一般近似O(n)。
$T3$以上正解来自官方题解。